package com.mapforge.map.tools;

import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.mapforge.listener.RenderEvent;
import com.mapforge.listener.RenderListener;
import com.mapforge.listener.TileMapCreationEvent;
import com.mapforge.listener.TileMapCreationListener;
import com.mapforge.map.Tile;
import com.mapforge.map.TileLayer;
import com.mapforge.map.Tileset;
import com.mapforge.map.tools.undo.TilePoint;
import com.mapforge.selection.ComparablePoint;
import com.mapforge.selection.DiscontinuousSelectionList;
import com.mapforge.selection.Selection;

/**
 * The {@link TileClipboard} class represents a clipboard to used to perform
 * cut/copy/paste operations with {@link Tile tiles.}
 * 
 * @author Will Morrison
 * 
 */
public class TileClipboard implements Iterable<ComparablePoint>,
		TileMapCreationListener {
	private static TileClipboard clipboard;
	private final DiscontinuousSelectionList selection;
	private Tileset tileset;
	private List<RenderListener> listeners;

	/**
	 * Singleton constructor for {@link TileClipboard TileClipboard.}
	 */
	private TileClipboard() {
		this.selection = new DiscontinuousSelectionList();
		this.listeners = new ArrayList<RenderListener>();
	}

	public boolean addRenderListener(RenderListener renderListener) {
		return this.listeners.add(renderListener);
	}

	public void clearRenderListeners() {
		this.listeners.clear();
	}

	public boolean removeRenderListener(RenderListener renderListener) {
		return this.listeners.remove(renderListener);
	}

	public Rectangle getBoundingRectangle() {
		return this.selection.getBoundingRectangle();
	}

	/**
	 * Singleton handle access.
	 * 
	 * @return only instance of this class that will exist during runtime
	 */
	public synchronized static final TileClipboard getHandle() {
		if (TileClipboard.clipboard == null) {
			TileClipboard.clipboard = new TileClipboard();
		}
		return TileClipboard.clipboard;
	}

	protected void fireRenderEvent() {
		RenderEvent event = new RenderEvent(this);
		for (RenderListener e : listeners) {
			e.renderEventOccurred(event);
		}
	}

	public void setTileset(Tileset tileset) {
		this.tileset = tileset;
	}

	public Tileset getTileset() {
		return this.tileset;
	}


	public void cutToClipboard(Selection clipArea, TileLayer layer) {
		this.selection.clearSelection();
		Tile[][] data = layer.getData();
		ComparablePoint p;
		Tile tile;
		Rectangle rect = clipArea.getBoundingRectangle();
		Point origin = new Point(rect.x, rect.y);
		Iterator<ComparablePoint> it = clipArea.iterator();
		Tile emptyTile = this.getTileset().getEmptyTile();
		while (it.hasNext()) {
			p = it.next();
			tile = data[p.y][p.x];
			// make coordinates relative to bounding rectangle origin
			this.selection.addPoint(new TilePoint(tile, p.x - origin.x, p.y
					- origin.y));
			// clear selection in Tileset
			layer.setTile(p.x, p.y, emptyTile);
		}
		this.fireRenderEvent();
	}

	/**
	 * Stores the specified {@link Selection} from the specified
	 * {@link TileLayer} in this {@link TileClipboard TileClipboard.}
	 * 
	 * @param selection
	 *            specified {@link Selection}
	 * @param layer
	 *            specified {@link TileLaye
	 */
	public void copyToClipboard(Selection clipArea, TileLayer layer) {
		this.selection.clearSelection();
		Tile[][] data = layer.getData();
		Iterator<ComparablePoint> it = clipArea.iterator();
		ComparablePoint p;
		Tile tile;
		Rectangle rect = clipArea.getBoundingRectangle();
		Point origin = new Point(rect.x, rect.y);
		while (it.hasNext()) {
			p = it.next();
			tile = data[p.y][p.x];
			// make coordinates relative to bounding rectangle origin
			this.selection.addPoint(new TilePoint(tile, p.x - origin.x, p.y
					- origin.y));
		}
	}

	public void writeClipboardSelection(Point point, TileLayer layer) {
		Iterator<ComparablePoint> it = this.selection.iterator();
		Rectangle rect = this.selection.getBoundingRectangle();
		Point origin = new Point(rect.x, rect.y);
		Tile emptyTile = this.tileset.getEmptyTile();
		int dx, dy;
		TilePoint p;
		int width = layer.getTotalHorizontalTiles();
		int height = layer.getTotalVerticalTiles();
		while (it.hasNext()) {
			p = (TilePoint) it.next();
			if (emptyTile != p.tile) {
				dx = point.x + (p.x - origin.x);
				dy = point.y + (p.y - origin.y);
				if (dx < width && dy < height) {
					layer.setTile(dx, dy, p.tile);
				}
			}
		}
		fireRenderEvent();
	}

	public void clearClipboard() {
		this.selection.clearSelection();
	}

	/**
	 * Returns whether the {@link TileClipboard} is holding {@link Tile tiles.}
	 * 
	 * @return whether the {@link TileClipboard is holding {@link Tile tiles.}
	 */
	public boolean isEmpty() {
		return this.selection.isEmpty();
	}

	@Override
	public Iterator<ComparablePoint> iterator() {
		return this.selection.iterator();
	}

	@Override
	public void tileMapCreationPerformed(TileMapCreationEvent mapCreationEvent) {
		this.tileset = mapCreationEvent.getTileMapCreated().getTileset();
	}

	/**
	 * Singleton instance, cannot clone.
	 */
	public Object clone() throws CloneNotSupportedException {
		throw new CloneNotSupportedException();
	}

}
